#include "ServerIoService.h"

#include <boost/process/search_path.hpp>
#include <boost/bind/bind.hpp>
#ifdef CROSS_WIN
#include <boost/process/windows.hpp>
#endif

#include <sstream>
#include <iostream>
#include <string>
#include <optional>
#include <thread>
#include <stdexcept>
#include <string_view>

namespace process = boost::process;
namespace asio = boost::asio;

using namespace simodo::edit::plugin::model_server;
using namespace std;

namespace
{
    std::string endlString()
    {
        std::stringstream ss;
        ss << std::endl;
        return ss.str();
    }

    std::string endl_str = endlString();
}

ServerIoService::ServerIoService(boost::asio::io_service & io
                               , std::string process_path
                               , std::vector<std::string> process_args)
    : _io(io)
    , _process_path(process_path)
    , _process_args(process_args)
{}

ServerIoService::~ServerIoService()
{
    stop();
}

void ServerIoService::write(string message)
{
    std::lock_guard lock(_m);

    if (!_server && message.empty())
    {
        return;
    }

    if (_server && !_server->running())
    {
        _stop();

        if (message.empty())
        {
            return;
        }
    }

    if (!_server)
    {
        _launchServer();
    }

    std::string str = (message + endl_str);

    _io.post([this, str]
    {
        std::lock_guard lock(_m);

        if (!_server || !_server->running())
        {
            return;
        }

        boost::system::error_code ec;
        asio::write(*_server_input, asio::buffer(str), ec);
        if (ec.failed())
        {
            _stop();
        }
    });
}

void ServerIoService::stop()
{
    std::lock_guard lock(_m);
    _stop();
}

void ServerIoService::_stop()
{
    if (!_server)
    {
        return;
    }

    _server_input->close();
    _server_output->close();

    _server_input = nullptr;
    _server_output = nullptr;

    std::error_code ec;
    // @todo Перейти на boost::process::v2
    // if (!_group->wait_for(100ms, ec))
    std::this_thread::sleep_for(100ms);
    if (_server->running(ec))
    {
        _group->terminate(ec);
    }
    // avoid zombie
    _server->wait(ec);

    _group = nullptr;
    _server = nullptr;
}

void ServerIoService::_launchServer()
{
    string server_exe = "simodo-server";
    auto server = process::search_path(server_exe);
    if (server.empty())
    {
        server = _process_path;
        if (server.empty())
            throw logic_error(server_exe + " " + tr("not found").toStdString());
    }

    _server_input = make_shared<process::async_pipe>(_io);
    _server_output = make_shared<process::async_pipe>(_io);

    _group = make_shared<process::group>();
    try
    {
        _server = make_shared<process::child>( server
                                             , _process_args
                                             , process::std_in < (*_server_input)
                                             , process::std_out > (*_server_output)
                                             , *_group
#ifdef CROSS_WIN
                                             , process::windows::hide
#endif
                                             );
    }
    catch (boost::process::process_error & e)
    {
        throw std::logic_error("Error while loading server: " + server.string() + ": " + e.what());
    }

    _postAsyncRead();
}

void ServerIoService::_asyncRead(const boost::system::error_code & ec, size_t size)
{
    if (ec.failed())
    {
        emit recieved(QVariant::fromValue(nullptr));
        return;
    }

    auto sob_beg_it = _server_output_buffer.begin();
    auto sob_size_it = sob_beg_it + size;

    std::string line(sob_beg_it, sob_size_it - endl_str.size());
    _server_output_buffer.erase(sob_beg_it, sob_size_it);
    
    emit recieved(QString::fromStdString(line));

    _postAsyncRead();
}

void ServerIoService::_postAsyncRead()
{
    asio::async_read_until( *_server_output
                          , asio::dynamic_buffer(_server_output_buffer)
                          , endl_str
                          , boost::bind( &ServerIoService::_asyncRead
                                       , this
                                       , asio::placeholders::error
                                       , asio::placeholders::bytes_transferred
                                       )
                          );
}
